Index: main/plugin-user-directory/doc/user-populations.sample.xml
===================================================================
--- main/plugin-user-directory/doc/user-populations.sample.xml	(revision 41094)
+++ main/plugin-user-directory/doc/user-populations.sample.xml	(working copy)
@@ -35,10 +35,10 @@
             <ldapScope>population.employee.ldap.scope</ldapScope>
         </parameters>
         <search-criteria>
-            <criteria metadata-ref="title"/>
+            <criteria metadata-ref="title" analyzed="false"/>
             <criteria metadata-ref="civility" />
-            <criteria metadata-ref="firstname"/>
-            <criteria metadata-ref="lastname" />
+            <criteria metadata-ref="firstname" analyzed="false"/>
+            <criteria metadata-ref="lastname" analyzed="false"/>
             <criteria metadata-ref="affectation" />
         </search-criteria>
         <result-columns>
Index: main/plugin-user-directory/plugin.xml
===================================================================
--- main/plugin-user-directory/plugin.xml	(revision 40335)
+++ main/plugin-user-directory/plugin.xml	(working copy)
@@ -423,6 +423,13 @@
             </components>
         </feature>
         
+        <feature name="user-metadata-indexer">
+            <components>
+                <component role="org.ametys.plugins.userdirectory.contenttype.UserMetadataIndexer"
+                           class="org.ametys.plugins.userdirectory.contenttype.UserMetadataIndexer"/>
+            </components>
+        </feature>
+        
         <feature name="userinterface.content.tab">
             <extensions>
                 <extension id="org.ametys.plugins.user.directory.content.Tab"
Index: main/plugin-user-directory/src/org/ametys/plugins/userdirectory/component/PopulationManager.java
===================================================================
--- main/plugin-user-directory/src/org/ametys/plugins/userdirectory/component/PopulationManager.java	(revision 41094)
+++ main/plugin-user-directory/src/org/ametys/plugins/userdirectory/component/PopulationManager.java	(working copy)
@@ -174,7 +174,7 @@
         for (String contentType : population.getContentTypes())
         {
             ContentType cType = _cTypeEP.getExtension(contentType);
-            for (String metadataPath : population.getSearchCriteriaMetadataPath())
+            for (String metadataPath : population.getSearchCriteriaMetadataPath().keySet())
             {
                 if (ContentTypeHelper.getMetadataDefinitionByPath(cType, metadataPath) == null)
                 {
Index: main/plugin-user-directory/src/org/ametys/plugins/userdirectory/contenttype/UserContentIndexer.java
===================================================================
--- main/plugin-user-directory/src/org/ametys/plugins/userdirectory/contenttype/UserContentIndexer.java	(revision 40174)
+++ main/plugin-user-directory/src/org/ametys/plugins/userdirectory/contenttype/UserContentIndexer.java	(working copy)
@@ -17,6 +17,8 @@
 
 import java.util.Map;
 
+import org.apache.avalon.framework.service.ServiceException;
+import org.apache.avalon.framework.service.ServiceManager;
 import org.apache.lucene.document.Document;
 import org.apache.lucene.document.Field;
 import org.apache.lucene.document.Field.Index;
@@ -41,6 +43,13 @@
     public static final String POPULATION = "population";
     
     @Override
+    public void service(ServiceManager manager) throws ServiceException
+    {
+        super.service(manager);
+        _metadataIndexer = (UserMetadataIndexer) manager.lookup(UserMetadataIndexer.ROLE);
+    }
+    
+    @Override
     public void indexContent(Content content, Document document, Map<String, Object> parameters) throws Exception
     {
         super.indexContent(content, document, parameters);
Index: main/plugin-user-directory/src/org/ametys/plugins/userdirectory/contenttype/UserMetadataIndexer.java
===================================================================
--- main/plugin-user-directory/src/org/ametys/plugins/userdirectory/contenttype/UserMetadataIndexer.java	(revision 0)
+++ main/plugin-user-directory/src/org/ametys/plugins/userdirectory/contenttype/UserMetadataIndexer.java	(revision 0)
@@ -0,0 +1,87 @@
+/*
+ *  Copyright 2016 Anyware Services
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.ametys.plugins.userdirectory.contenttype;
+
+import java.text.Normalizer;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.Field.Index;
+import org.apache.lucene.document.Field.Store;
+
+import org.ametys.cms.contenttype.MetadataDefinition;
+import org.ametys.cms.lucene.FieldNames;
+import org.ametys.cms.lucene.SynchronizedMetadataIndexer;
+import org.ametys.cms.repository.Content;
+import org.ametys.plugins.repository.metadata.CompositeMetadata;
+import org.ametys.runtime.util.I18nizableText;
+import org.ametys.runtime.util.parameter.Enumerator;
+
+/**
+ * Specific {@link SynchronizedMetadataIndexer} for user contents.
+ */
+public class UserMetadataIndexer extends SynchronizedMetadataIndexer
+{
+    /** The avalon role. */
+    @SuppressWarnings("hiding")
+    public static final String ROLE = UserMetadataIndexer.class.getName();
+    
+    /** The "-not-analysed" suffix */
+    public static final String NOT_ANALYZED_SUFFIX = "-not-analyzed";
+    
+    @Override
+    public void indexStringMetadata(CompositeMetadata metadata, String prefix, Content content, Document document, String name, String fieldName, MetadataDefinition definition) throws Exception
+    {
+        String[] strValues = metadata.getStringArray(name, new String[0]);
+        for (String value : strValues)
+        {
+            document.add(new Field(getGlobalPrefix() + FieldNames.FULL, value, Store.NO, Index.ANALYZED));
+            
+            Enumerator enumerator = definition.getEnumerator();
+            document.add(new Field(getGlobalPrefix() + prefix + fieldName, value, Store.YES, enumerator == null ? Index.ANALYZED : Index.NOT_ANALYZED));
+            
+            if (enumerator == null)
+            {
+                String newValue = Normalizer.normalize(value.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim();
+                document.add(new Field(getGlobalPrefix() + prefix + fieldName + NOT_ANALYZED_SUFFIX, newValue, Store.YES, Index.NOT_ANALYZED));
+            }
+            
+            // Index "-for-sorting" field only for single metadata and DO NOT index a 'title-for-sorting' field (reserved field for page sorting)
+            if (strValues.length == 1 && !"title".equals(prefix + fieldName))
+            {
+                if (enumerator != null)
+                {
+                    I18nizableText label = enumerator.getEntry(value);
+                    if (label != null)
+                    {
+                        document.add(new Field(getGlobalPrefix() + prefix + fieldName + _FOR_SORTING_SUFFIX, _i18nUtils.translate(label, content.getLanguage()), Store.YES, Index.NOT_ANALYZED));
+                    }
+                }
+                else
+                {
+                    document.add(new Field(getGlobalPrefix() + prefix + fieldName + _FOR_SORTING_SUFFIX, value, Store.YES, Index.NOT_ANALYZED));
+                }
+            }
+            
+            String[] words = StringUtils.split(value);
+            for (String word : words)
+            {
+                document.add(new Field(getGlobalPrefix() + FieldNames.ALL_NOT_ANALYZED, StringUtils.strip(word, ",?!:()[].{}=").toLowerCase(), Store.NO, Index.NOT_ANALYZED));
+            }
+        }
+    }
+}
Index: main/plugin-user-directory/src/org/ametys/plugins/userdirectory/generator/UserDirectorySearchGenerator.java
===================================================================
--- main/plugin-user-directory/src/org/ametys/plugins/userdirectory/generator/UserDirectorySearchGenerator.java	(revision 41094)
+++ main/plugin-user-directory/src/org/ametys/plugins/userdirectory/generator/UserDirectorySearchGenerator.java	(working copy)
@@ -16,6 +16,7 @@
 package org.ametys.plugins.userdirectory.generator;
 
 import java.io.IOException;
+import java.text.Normalizer;
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.Map;
@@ -40,15 +41,18 @@
 import org.apache.lucene.search.SortField;
 import org.apache.lucene.search.TermQuery;
 import org.apache.lucene.search.TermRangeQuery;
+import org.apache.lucene.search.WildcardQuery;
 import org.xml.sax.SAXException;
 
 import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
 import org.ametys.cms.contenttype.MetadataDefinition;
 import org.ametys.cms.contenttype.MetadataType;
+import org.ametys.cms.lucene.FieldNames;
 import org.ametys.cms.repository.ContentTypeHelper;
 import org.ametys.plugins.userdirectory.component.PopulationManager;
 import org.ametys.plugins.userdirectory.contenttype.UserContentIndexer;
+import org.ametys.plugins.userdirectory.contenttype.UserMetadataIndexer;
 import org.ametys.plugins.userdirectory.population.Population;
 import org.ametys.runtime.util.I18nizableText;
 import org.ametys.runtime.util.parameter.Enumerator;
@@ -151,7 +155,7 @@
         String contentType = population.getContentTypes().get(0);
         ContentType cType = _ctypeEP.getExtension(contentType);
         
-        for (String metadataPath : population.getSearchCriteriaMetadataPath())
+        for (String metadataPath : population.getSearchCriteriaMetadataPath().keySet())
         {
             MetadataDefinition metadataDef = ContentTypeHelper.getMetadataDefinitionByPath(cType, metadataPath);
             
@@ -233,7 +237,8 @@
         
         Population population = _populationManager.getPopulations().get(populationId);
         String contentTypeId = population.getContentTypes().get(0);
-        for (String metadataPath : population.getSearchCriteriaMetadataPath())
+        Map<String, Map<String, String>> searchCriteriaMetadataPath = population.getSearchCriteriaMetadataPath();
+        for (String metadataPath : searchCriteriaMetadataPath.keySet())
         {
             ContentType cType = _ctypeEP.getExtension(contentTypeId);
             MetadataDefinition metadataDef = ContentTypeHelper.getMetadataDefinitionByPath(cType, metadataPath);
@@ -255,7 +260,8 @@
                         __addDateFieldQuery(query, request, metadataPath);
                         break;
                     default:
-                        _addTextFieldQuery(query, request, metadataPath);
+                        Map<String, String> attributes = searchCriteriaMetadataPath.get(metadataPath);
+                        _addTextFieldQuery(query, request, metadataPath, attributes);
                         break;
                 }
             }
@@ -327,10 +333,11 @@
      * @param query The query
      * @param request The request
      * @param param The field's name
+     * @param attributes the additional attribute
      * @throws ParseException if failed to parse value
      * @throws IllegalArgumentException if the field's value does not match the excepted pattern
      */
-    protected void _addTextFieldQuery(BooleanQuery query, Request request, String param) throws ParseException, IllegalArgumentException
+    protected void _addTextFieldQuery(BooleanQuery query, Request request, String param, Map<String, String> attributes) throws ParseException, IllegalArgumentException
     {
         String value = request.getParameter(__FIELD_PREFIX + param);
         
@@ -342,11 +349,25 @@
                 throw new IllegalArgumentException(value + " does not match the expected regular expression : " + _TEXTFIELD_PATTERN.pattern());
             }
             
-            
-            QueryParser parser = new QueryParser(LuceneConstants.LUCENE_VERSION, param, _analyser);
-            Query queryField = parser.parse(value + "*");
-            query.add(queryField, BooleanClause.Occur.MUST);
+            boolean isAnalyzed = true;
+            if (attributes.containsKey("analyzed") && attributes.get("analyzed").equals("false"))
+            {
+                isAnalyzed = false;
+            }
             
+            if (isAnalyzed)
+            {
+                QueryParser parser = new QueryParser(LuceneConstants.LUCENE_VERSION, param, _analyser);
+                Query queryField = parser.parse(QueryParser.escape(value));
+                query.add(queryField, BooleanClause.Occur.MUST);
+            }
+            else
+            {
+                String newValue = Normalizer.normalize(value.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim();
+                Term term = new Term(param + UserMetadataIndexer.NOT_ANALYZED_SUFFIX, newValue + "*");
+                WildcardQuery wildcarQuery = new WildcardQuery(term);
+                query.add(wildcarQuery, BooleanClause.Occur.MUST);
+            }
         }
     }
     
@@ -419,7 +440,7 @@
         String idPopulation = zoneItem.getServiceParameters().getString("population");
         Population population = _populationManager.getPopulations().get(idPopulation);
         
-        for (String metadataPath : population.getSearchCriteriaMetadataPath())
+        for (String metadataPath : population.getSearchCriteriaMetadataPath().keySet())
         {
             String parameterValue = request.getParameter(__FIELD_PREFIX + metadataPath);
             if (StringUtils.isNotBlank(parameterValue))
Index: main/plugin-user-directory/src/org/ametys/plugins/userdirectory/population/AbstractPopulation.java
===================================================================
--- main/plugin-user-directory/src/org/ametys/plugins/userdirectory/population/AbstractPopulation.java	(revision 41094)
+++ main/plugin-user-directory/src/org/ametys/plugins/userdirectory/population/AbstractPopulation.java	(working copy)
@@ -15,6 +15,7 @@
  */
 package org.ametys.plugins.userdirectory.population;
 
+import java.io.IOException;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Date;
@@ -28,14 +29,15 @@
 import org.apache.avalon.framework.configuration.Configurable;
 import org.apache.avalon.framework.configuration.Configuration;
 import org.apache.avalon.framework.configuration.ConfigurationException;
-import org.apache.avalon.framework.context.Context;
 import org.apache.avalon.framework.context.ContextException;
 import org.apache.avalon.framework.context.Contextualizable;
 import org.apache.avalon.framework.logger.Logger;
 import org.apache.avalon.framework.service.ServiceException;
 import org.apache.avalon.framework.service.ServiceManager;
 import org.apache.avalon.framework.service.Serviceable;
+import org.apache.cocoon.environment.Context;
 import org.apache.commons.lang.StringUtils;
+import org.apache.lucene.index.IndexWriter;
 
 import org.ametys.cms.contenttype.ContentType;
 import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
@@ -66,6 +68,8 @@
 import org.ametys.runtime.config.Config;
 import org.ametys.runtime.user.CurrentUserProvider;
 import org.ametys.runtime.util.I18nizableText;
+import org.ametys.web.lucene.IndexerHelper;
+import org.ametys.web.lucene.PageIndexer;
 import org.ametys.web.repository.page.Page;
 
 import com.opensymphony.workflow.InvalidActionException;
@@ -86,7 +90,7 @@
     /** The population content types */
     protected List<String> _contentTypes;
     /** The population search criteria metadata path */
-    protected List<String> _searchCriteriaMetadataPath;
+    protected Map<String, Map<String, String>> _searchCriteriaMetadataPath;
     /** The population result column metadata path */
     protected Map<String, Map<String, String>> _resultColumnsMetadataPath;
     /** The population mapping */
@@ -98,7 +102,9 @@
     /** The population unlisted metadata path */
     protected String _unlistedMetadataPath;
     /** The context */
-    protected Context _context;
+    protected org.apache.avalon.framework.context.Context _context;
+    /** Cocoon context */
+    protected Context _cocoonContext;
     /** The language manager */
     protected LanguagesManager _languagesManager;
     /** The ametys object resolver */
@@ -115,13 +121,16 @@
     protected UserDirectoryPageHandler _udPageHandler;
     /** The service manager */
     protected ServiceManager _serviceManager;
-
+    /** The page indexer */
+    protected PageIndexer _pageIndexer;
+    
     /** The population manager */
     private PopulationManager _populationManager;
     
-    public void contextualize(Context context) throws ContextException
+    public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException
     {
         _context = context;
+        _cocoonContext = (Context) context.get(org.apache.cocoon.Constants.CONTEXT_ENVIRONMENT_CONTEXT);
     }
     
     @Override
@@ -133,6 +142,8 @@
         _workflowManager = (Workflow) serviceManager.lookup(Workflow.ROLE);
         _observationManager = (ObservationManager) serviceManager.lookup(ObservationManager.ROLE);
         _currentUserProvider = (CurrentUserProvider) serviceManager.lookup(CurrentUserProvider.ROLE);
+        _pageIndexer = (PageIndexer) serviceManager.lookup(PageIndexer.ROLE);
+        
         _serviceManager = serviceManager;
     }
     
@@ -170,7 +181,7 @@
     }
     
     @Override
-    public List<String> getSearchCriteriaMetadataPath()
+    public Map<String, Map<String, String>> getSearchCriteriaMetadataPath()
     {
         return _searchCriteriaMetadataPath;
     }
@@ -268,7 +279,7 @@
      */
     protected void _configureSearchCriteria(Configuration configuration) throws ConfigurationException
     {
-        _searchCriteriaMetadataPath = new ArrayList<String>();
+        _searchCriteriaMetadataPath = new HashMap<String, Map<String, String>>();
         Configuration[] searchCriteria = configuration.getChild("search-criteria").getChildren("criteria");
         if (searchCriteria.length == 0)
         {
@@ -279,7 +290,12 @@
             for (Configuration criteriaConf : searchCriteria)
             {
                 String metadataRef = criteriaConf.getAttribute("metadata-ref");
-                _searchCriteriaMetadataPath.add(metadataRef);
+                
+                Map<String, String> attributes = new HashMap<String, String>();
+                String analyzed = criteriaConf.getAttribute("analyzed", "true");
+                attributes.put("analyzed", analyzed);
+                
+                _searchCriteriaMetadataPath.put(metadataRef, attributes);
             }
         }
     }
@@ -897,7 +913,34 @@
             Set<Page> udRootPages = _udPageHandler.getUserDirectoryRootPages (this.getId());
             for (Page rootPage : udRootPages)
             {
-                _observationManager.notify(new Event(_currentUserProvider.getUser(), org.ametys.web.ObservationConstants.PAGE_CHANGED, rootPage, new Object[]{rootPage.getId()}));
+                IndexWriter indexWriter = null;
+                try
+                {
+                    indexWriter = IndexerHelper.getIndexWriter(_cocoonContext, _pageIndexer, rootPage.getSiteName(), rootPage.getSitemapName());
+                    IndexerHelper.unIndexPage(rootPage, indexWriter, logger);
+
+                    _observationManager.notify(new Event(_currentUserProvider.getUser(), org.ametys.web.ObservationConstants.PAGE_CHANGED, rootPage, new Object[]{rootPage.getId()}));
+                }
+                catch (IOException e)
+                {
+                    syncReport.incrementNbError();
+                    logger.error("An error occurred indexing root page : " + rootPage.getName(), e);
+                }
+                finally
+                {
+                    if (indexWriter != null)
+                    {
+                        try
+                        {
+                            indexWriter.close();
+                        }
+                        catch (Exception e)
+                        {
+                            syncReport.incrementNbError();
+                            logger.error("Unable to close index for root page : " + rootPage.getName(), e);
+                        }
+                    }
+                }
             }
         }
         catch (ServiceException e)
Index: main/plugin-user-directory/src/org/ametys/plugins/userdirectory/population/Population.java
===================================================================
--- main/plugin-user-directory/src/org/ametys/plugins/userdirectory/population/Population.java	(revision 41094)
+++ main/plugin-user-directory/src/org/ametys/plugins/userdirectory/population/Population.java	(working copy)
@@ -57,7 +57,7 @@
      * Get the paths of metadata to be used for search criteria.
      * @return paths of metadata
      */
-    List<String> getSearchCriteriaMetadataPath();
+    Map<String, Map<String, String>> getSearchCriteriaMetadataPath();
     
     /**
      * Get the paths of metadata to be used for search result columns.
